import localforage from 'localforage';


export enum ChildType {
  Invalid = 0,
  File = 1,
  Dir = 2,
};
export interface IFileInfo {
  parent_id: string;
  id: string;
  name: string;
  type: ChildType;
}
export default class Workspace {

  private static _insts = new Map<string, Promise<Workspace> | Workspace>()
  static find(name: string): Workspace | undefined {
    const i = this._insts.get(name);
    return i instanceof Promise ? void 0 : i;
  }

  static async get(name: string): Promise<Workspace> {
    const inst = this._insts.get(name)
    if (inst) return inst;
    const p = new Workspace(name);
    this._insts.set(name, p)
    return p._init().then(r => {
      this._insts.set(name, r)
      return r
    }).catch(e => {
      this._insts.delete(name)
      throw e
    })
  }

  private _last_id: number = 1
  readonly _relations: LocalForage;
  readonly _misc: LocalForage;
  readonly _infos: LocalForage;
  readonly _files: LocalForage;

  private constructor(name: string) {
    this._relations = localforage.createInstance({ name, storeName: 'relations' })
    this._infos = localforage.createInstance({ name, storeName: 'infos' })
    this._files = localforage.createInstance({ name, storeName: 'files' })
    this._misc = localforage.createInstance({ name, storeName: 'misc' })
  }
  private async _init(): Promise<this> {
    this._last_id = await this._misc.getItem<number>('last_id').then(v => v ?? 1);
    return this
  }
  private async get_last_id(): Promise<string> {
    await this._misc.setItem('last_id', this._last_id + 1);
    return '' + (++this._last_id)
  }
  private _sort_chilren(children: IFileInfo[]) {
    children.sort((a, b) => {
      if (a.type !== b.type)
        return b.type - a.type;
      const len = Math.max(a.name.length, b.name.length);
      for (let i = 0; i < len; ++i) {
        const c_1 = a.name.charCodeAt(i);
        const c_2 = b.name.charCodeAt(i);
        if (Number.isNaN(c_1)) return -1;
        if (Number.isNaN(c_2)) return 1;
        return c_1 - c_2
      }
      return 0;
    })
  }

  async list(id: string = ''): Promise<IFileInfo[]> {
    const children = await this._relations.getItem<IFileInfo[]>(id)
    if (!children && id) throw new Error(`[list] id:"${id}" not found`)
    return children ?? [];
  }

  async files(id: string = ''): Promise<IFileInfo[]> {
    return this.list(id).then(children => children.filter(v => v.type === ChildType.File))
  }

  async directories(id: string = ''): Promise<IFileInfo[]> {
    if (typeof id !== 'string') throw new Error('id must be a string!')
    return this.list(id).then(children => children.filter(v => v.type === ChildType.Dir))
  }

  async new_child(parent_id: string, name: string, type: ChildType): Promise<string> {
    if (typeof parent_id !== 'string') throw new Error('parent_id must be a string!');
    if (typeof name !== 'string') throw new Error('name must be a string!');
    const children = await this.list(parent_id);
    const duplicated = !!children.find(v => v.name === name);
    if (duplicated) throw new Error('name must be not duplicated!');

    const id = await this.get_last_id();
    const new_born: IFileInfo = { parent_id, id, name, type }
    children.push(new_born);
    this._sort_chilren(children);
    await this._relations.setItem(parent_id, children);
    await this._infos.setItem(id, new_born);
    if (type === ChildType.Dir) this._relations.setItem(id, []);
    if (type === ChildType.File) this.save_file(id, "");
    return id;
  }

  async new_file(parent_id: string, name: string): Promise<string> {
    if (typeof parent_id !== 'string') throw new Error('parent_id must be a string!')
    if (typeof name !== 'string') throw new Error('name must be a string!')
    return await this.new_child(parent_id, name, ChildType.File)
  }

  async new_dir(parent_id: string, name: string): Promise<string> {
    if (typeof parent_id !== 'string') throw new Error('parent_id must be a string!')
    if (typeof name !== 'string') throw new Error('name must be a string!')
    return await this.new_child(parent_id, name, ChildType.Dir)
  }

  async info(id: string): Promise<IFileInfo> {
    if (typeof id !== 'string') throw new Error('id must be a string!')
    const ret = await this._infos.getItem<IFileInfo>(id)
    if (!ret) throw new Error(`[info] id:"${id}" not found`)
    return ret;
  }

  async delete(id: string): Promise<void> {
    if (typeof id !== 'string') throw new Error('id must be a string!')
    const { parent_id, type } = await this.info(id)
    const brothers = await this.list(parent_id);

    if (type === ChildType.Dir) {
      const children = await this.list(id)
      for (const { id } of children) await this.delete(id)
      this._relations.removeItem(id)
    }

    // remove file content
    if (type === ChildType.File) {
      await this._files.removeItem(id)
    }

    // remove form parents;
    await this._relations.setItem(parent_id, brothers.filter(v => v.id !== id));

    // remove info
    await this._infos.removeItem(id);
  }

  async save_file(id: string, content: string): Promise<void> {
    if (typeof id !== 'string') throw new Error('id must be a string!')
    if (typeof content !== 'string') throw new Error('content must be a string!')
    await this._files.setItem(id, content)
  }

  async load_file(id: string): Promise<string> {
    if (typeof id !== 'string') throw new Error('id must be a string!')
    const ret = await this._files.getItem<string>(id)
    if (typeof ret !== 'string') throw new Error('file not exists!')
    return ret;
  }
  
  async move(id: string, new_parent_id: string): Promise<boolean> {
    if (typeof id !== 'string') throw new Error('id must be a string!')
    if (typeof new_parent_id !== 'string') throw new Error('new_parent_id must be a string!')
    if (id === '') throw new Error('root is not movable!')

    const target = await this.info(id)
    const old_parent_id = target.parent_id
    if (old_parent_id === new_parent_id) return false;
    if (target.type === ChildType.Dir) {
      // 防止目录被放到自己的子目录下。
      let parent_itr = await this.info(new_parent_id)
      while (true) {
        if (parent_itr.id === id) return false;
        if (parent_itr.parent_id === '') break;
        parent_itr = await this.info(parent_itr.parent_id)
      }
    }

    const old_brothers = await this.list(old_parent_id);
    const new_brothers = await this.list(new_parent_id);

    // take info from src dir
    const idx = old_brothers.findIndex(v => v.id === id)
    old_brothers.splice(idx, 1)
    target.parent_id = new_parent_id;

    const used_names = new Set();
    for (const { name } of new_brothers) used_names.add(name);

    if (target.type === ChildType.Dir) {
      const old_name = target.name;
      let new_name = old_name
      const reg = /\scopy\s*(\d*)$/
      while (used_names.has(new_name)) {
        const res = new_name.match(reg)
        new_name = old_name + ' copy';
        if (res) new_name += ' ' + (Number(res[1]) + 1)
      }
      target.name = new_name;
    }
    else if (target.type === ChildType.File) {
      const r = target.name.match(/(.*)(\..*?)$/)
      const old_name = r?.[1] ?? target.name;
      const suffix = r?.[2] ?? '';

      let new_name = old_name
      const reg = /\scopy\s*(\d*)$/
      while (used_names.has(new_name + suffix)) {
        const res = new_name.match(reg)
        new_name = old_name + ' copy';
        if (res) new_name += ' ' + (Number(res[1]) + 1)
      }
      target.name = new_name + suffix;
    }

    // put info to src dir
    new_brothers.push(target);
    this._sort_chilren(new_brothers)

    // save relations
    await this._relations.setItem(new_parent_id, new_brothers)
    await this._relations.setItem(old_parent_id, old_brothers)
    await this._infos.setItem(id, target);
    return true;
  }

  async rename(id: string, name: string): Promise<IFileInfo> {
    const info = await this.info(id);
    const brothers = await this.list(info.parent_id);
    const idx = brothers.findIndex(v => v.id === id)

    const used_names = new Set<string>()
    for (const { id: bid, name } of brothers) {
      if (bid !== id) used_names.add(name);
    }
    if (used_names.has(name))
      throw new Error('name must be not duplicated!');

    info.name = name
    brothers[idx].name = name
    this._sort_chilren(brothers)
    await this._relations.setItem(info.parent_id, brothers)
    return await this._infos.setItem(id, info)
  }
}